package com.just.springboottrans.service;

import com.just.springboottrans.dao.PersonRepository;
import com.just.springboottrans.domain.Person;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import java.util.Optional;

/**
 * 这篇先谈事务的隔离级别（isolation level）
 *
 * 数据库是要被广大客户所共享访问的，那么在数据库操作过程中很可能出现以下几种不确定情况。
 *
 * 更新丢失(Lost update)
 * 两个事务都同时更新一行数据，一个事务对数据的更新把另一个事务对数据的更新覆盖了。比如CMS系统中，两个同时打开一篇文章进行修改，一个人先保存，另一个人后保存，后保存的就覆盖了先保存的那个人的内容，这就造成更新丢失。
 * 这是因为系统没有执行任何的锁操作，因此并发事务并没有被隔离开来。在并发事务处理带来的问题中，“更新丢失”通常应该是完全避免的。但防止更新丢失，并不能单靠数据库事务控制器来解决，需要应用程序对要更新的数据加必要的锁来解决，因此，防止更新丢失应该是应用的责任。
 *
 * 脏读（Dirty reads）
 * 一个事务读取到了另一个事务未提交的数据操作结果。这是相当危险的，因为很可能所有的操作都被回滚。
 *
 * 不可重复读（Non-repeatable Reads）
 * 一个事务对同一行数据重复读取两次，但是却得到了不同的结果。比如事务T1读取某一数据后，事务T2对其做了修改，当事务T1再次读该数据时得到与前一次不同的值。又叫虚读。
 *
 * 幻读（Phantom Reads）
 * 事务在操作过程中进行两次查询，第二次查询的结果包含了第一次查询中未出现的数据或者缺少了第一次查询中出现的数据（这里并不要求两次查询的SQL语句相同）。这是因为在两次查询过程中有另外一个事务插入数据造成的。
 *
 * 不可重复读的重点是修改某个记录字段，幻读的重点在于新增或者删除记录。
 * 对于前者, 只需要锁住满足条件的记录。对于后者, 要锁住满足条件及其相近的记录。
 *
 * “脏读”、“不可重复读”和“幻读”，其实都是数据库读一致性问题，必须由数据库提供一定的事务隔离机制来解决。
 */
@Service
public class PersonServiceImpl implements PersonService{
    @Autowired
    PersonRepository personRepository;
    @Autowired
    OtherService otherService;
    @Transactional(rollbackFor = IllegalArgumentException.class)
    @Override
    public Person savePersonWithRollback(Person person) {
        Person p=personRepository.save(person);
        if(p.getName().equals("二狗子")){
            throw new IllegalArgumentException("二狗子已经存在，数据将回滚");
        }
        return person;
    }

    @Transactional(noRollbackFor = IllegalArgumentException.class)
    @Override
    public Person savePersonWithoutRollback(Person person) {
        Person p=personRepository.save(person);
        if(p.getName().equals("二狗子")){
            throw new IllegalArgumentException("二狗子已经存在，数据将回滚");
        }
        return person;
    }

    /**
     * propagation定义了事务的生命周期
     * A调用B方法，B将和A使用相同的事务，B发生异常的时候A也回滚
     * 同一个Service中调用方法时：
     * 不论注解是Propagation.REQUIRES_NEW 还是 Propagation.REQUIRED，
     * 其结果都是一样的，就是都被忽略掉了，等于没写。
     */
    @Transactional(propagation = Propagation.REQUIRED)
    @Override
    public Person save1(Person person){
        Person p=personRepository.save(person);
        methodB(p);
        return p;
    }

    /**
     * propagation = Propagation.REQUIRES_NEW,这个在methodC中有定义
     * 对于方法A和B，如果A调用了B，无论是否有事务都加了不同的事务，A异常不会导致B回滚
     * 注意一定是不同的service，跟代理有关
     * 在测试的时候出现一个坑，methodD()里面加了Person参数，传了和methodC相同的Person，最后数据库数据只有1条,这个和jpa有关
     */
    @Transactional
    @Override
    public Person save2(Person person) {
        otherService.methodC(person);      //不回滚
        otherService.methodD();            //不回滚
        throw new IllegalArgumentException("哎呦喂，主方法发生异常，回滚");
    }

    /**
     * 测试Propagation.NEVER
     * 有事务会报错，此处必须没事务
     */
    @Transactional(propagation = Propagation.NEVER)
    @Override
    public Person save3(Person person) {
        Person p=personRepository.save(person);
        if(p.getAge()==8){
            throw new IllegalArgumentException("年龄太小，回滚");
        }
        return p;
    }

    /**
     * 测试Propagation.MANDATORY
     * methodE方法上定义
     * 如果没有事务，会先抛错
     */
    @Transactional
    @Override
    public Person save4(Person person) {
        otherService.methodE(person);
        if(person.getAge()==8){
            throw new IllegalArgumentException("年龄太小，回滚");
        }
        return person;
    }

    /**
     * 测试Propagation.SUPPORTS
     * 有事务就使用事务，没有就不用
     */
    @Transactional(propagation = Propagation.SUPPORTS)
    @Override
    public Person save5(Person person) {
        otherService.methodF();
        //otherService.methodC(person);
        if(person.getAge()==8){
            throw new IllegalArgumentException("年龄太小，回滚");
        }
        return person;
    }

    /**
     * 隔离决定了事务的完整性，处理在多事务对相同数据下的处理机制
     * READ_UNCOMMITTED，A事务修改了一条记录但是没有提交事务，B事务可以读取到修改后的记录
     * 可导致脏读，不可重复读以及幻读
     */
    @Transactional(isolation = Isolation.READ_UNCOMMITTED)
    @Override
    public Person testIsolation1(Person person) {
        Person p=personRepository.saveAndFlush(person);
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("修改完毕");
        return p;
    }

    /**
     * READ_COMMITTED，只有A事务修改记录并提交之后，B事务才能读取到修改后的数据
     * 阻止脏读，但是可能导致不可重复读以及幻读
     */
    @Transactional
    @Override
    public Person testIsolation2(Person person) {
        Person p=personRepository.save(person);
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("修改完毕");
        return p;
    }

    /**
     *  REPEATABLE_READ，可重复读，取数据的事务将会禁止写事务（但允许读事务），写事务则禁止任何其他事务
     *  可能导致幻读
     */
    @Transactional(isolation = Isolation.REPEATABLE_READ)
    @Override
    public Object testIsolation3(Long id) {
        Optional<Person> p=personRepository.findById(id);
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("修改完毕");
        return p.isPresent()?p.get():"无数据";
    }

    /**
     * SERIALIZABLE，序列化
     * 提供严格的事务隔离。它要求事务序列化执行，事务只能一个接着一个地执行，不能并发执行。
     */
    @Transactional(isolation = Isolation.SERIALIZABLE)
    @Override
    public Person testIsolation4(Person person) {
        Person p=personRepository.saveAndFlush(person);
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("修改完毕");
        return p;
    }

    /**
     * DEFAULT，数据库默认的隔离级别
     *  mysql默认是REPEATABLE_READ
     */
    @Transactional(isolation = Isolation.DEFAULT)
    @Override
    public Person testIsolation5(Person person) {
        Person p=personRepository.saveAndFlush(person);
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("修改完毕");
        return p;
    }
    @Transactional(isolation = Isolation.READ_UNCOMMITTED)
    @Override
    public Person findOne(Long id) {
        Optional<Person> personOption=personRepository.findById(id);
        return personOption.orElse(null);
    }

    /**
     * 设置超时时间，经过测试，单位是秒
     */
    @Transactional(timeout = 1)
    @Override
    public Person save6(Person person) {
        Person p=personRepository.saveAndFlush(person);
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return p;
    }


    public void methodB(Person p){
        if("二狗子".equals(p.getName())){
            throw new IllegalArgumentException("二狗子不合法，方法B回滚");
        }
    }
}
